import os
import sys
import tomllib
from fnmatch import fnmatch
from pathlib import Path

import pytest

from cibuildwheel.__main__ import main
from cibuildwheel.environment import ParsedEnvironment
from cibuildwheel.frontend import _split_config_settings, parse_config_settings
from cibuildwheel.options import BuildOptions, _get_pinned_container_images
from cibuildwheel.selector import BuildSelector, EnableGroup
from cibuildwheel.util import resources
from cibuildwheel.util.packaging import DependencyConstraints

# CIBW_PLATFORM is tested in main_platform_test.py


def test_old_free_threaded(monkeypatch, capsys):
    monkeypatch.setenv("CIBW_FREE_THREADED_SUPPORT", "ON")

    with pytest.raises(SystemExit):
        main()

    assert (
        "CIBW_FREE_THREADED_SUPPORT environment variable is no longer supported."
        in capsys.readouterr().err
    )


@pytest.mark.usefixtures("platform")
def test_output_dir(intercepted_build_args, monkeypatch):
    OUTPUT_DIR = Path("some_output_dir")

    monkeypatch.setenv("CIBW_OUTPUT_DIR", str(OUTPUT_DIR))

    main()

    assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR.resolve()


@pytest.mark.usefixtures("platform")
def test_output_dir_default(intercepted_build_args):
    main()

    assert intercepted_build_args.args[0].globals.output_dir == Path("wheelhouse").resolve()


@pytest.mark.usefixtures("platform")
@pytest.mark.parametrize("also_set_environment", [False, True])
def test_output_dir_argument(also_set_environment, intercepted_build_args, monkeypatch):
    OUTPUT_DIR = Path("some_output_dir")

    monkeypatch.setattr(sys, "argv", [*sys.argv, "--output-dir", str(OUTPUT_DIR)])
    if also_set_environment:
        monkeypatch.setenv("CIBW_OUTPUT_DIR", "not_this_output_dir")

    main()

    assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR.resolve()


@pytest.mark.usefixtures("platform", "allow_empty")
def test_build_selector(intercepted_build_args, monkeypatch):
    monkeypatch.setenv("CIBW_BUILD", "cp313-*")
    monkeypatch.setenv("CIBW_SKIP", "cp39-*")

    main()

    intercepted_build_selector = intercepted_build_args.args[0].globals.build_selector
    assert isinstance(intercepted_build_selector, BuildSelector)
    assert intercepted_build_selector("cp313-something-to-build")
    assert not intercepted_build_selector("cp39-something-to-skip")
    # This unit test is just testing the options of 'main'
    # Unit tests for BuildSelector are in build_selector_test.py


@pytest.mark.usefixtures("platform", "allow_empty")
def test_invalid_build_selector(monkeypatch, capsys):
    monkeypatch.setenv("CIBW_BUILD", "invalid")

    with pytest.raises(SystemExit) as e:
        main()

    assert e.value.code == 2
    _, err = capsys.readouterr()
    assert "Invalid build selector" in err


@pytest.mark.parametrize(
    ("option_name", "option_env_var"),
    [
        ("skip", "CIBW_SKIP"),
        ("test_skip", "CIBW_TEST_SKIP"),
    ],
)
@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_invalid_skip_selector(monkeypatch, capsys, option_name, option_env_var):
    monkeypatch.setenv(option_env_var, "invalid")

    main()

    _, err = capsys.readouterr()
    print(err)
    assert f"Invalid {option_name} selector" in err


@pytest.mark.parametrize(
    "selector", ["*-macosx_universal2:arm64", "*-macosx_universal2:x86_64", "*-macosx_arm64"]
)
@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_valid_test_skip_selector(monkeypatch, capsys, selector):
    monkeypatch.setenv("CIBW_TEST_SKIP", selector)

    main()

    _, err = capsys.readouterr()
    print(err)
    assert "Invalid test_skip selector" not in err


@pytest.mark.parametrize("selector", ["*-macosx_universal2:invalid", "*-macosx_arm64:arm64"])
@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_invalid_test_skip_selector(monkeypatch, capsys, selector):
    monkeypatch.setenv("CIBW_TEST_SKIP", selector)

    main()

    _, err = capsys.readouterr()
    print(err)
    assert "Invalid test_skip selector" in err


@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_empty_selector(monkeypatch):
    monkeypatch.setenv("CIBW_SKIP", "*")

    with pytest.raises(SystemExit) as e:
        main()

    assert e.value.code == 3


@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_riscv64_warning1(monkeypatch, capsys):
    monkeypatch.setenv("CIBW_ENABLE", "cpython-experimental-riscv64")

    main()

    _, err = capsys.readouterr()
    print(err)
    assert "'cpython-experimental-riscv64' enable is deprecated" in err


@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_riscv64_warning2(monkeypatch, capsys, tmp_path):
    local_path = tmp_path / "tmp_project"
    os.mkdir(local_path)  # noqa:PTH102 Path.mkdir has been monkeypatched already
    local_path.joinpath("setup.py").touch()

    monkeypatch.setattr(
        sys, "argv", ["cibuildwheel", "--only", "cp313-manylinux_riscv64", str(local_path)]
    )
    monkeypatch.setenv("CIBW_ENABLE", "cpython-experimental-riscv64")

    main()

    _, err = capsys.readouterr()
    print(err)
    assert "'cpython-experimental-riscv64' enable is deprecated" in err


@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_riscv64_no_warning(monkeypatch, capsys, tmp_path):
    local_path = tmp_path / "tmp_project"
    os.mkdir(local_path)  # noqa:PTH102 Path.mkdir has been monkeypatched already
    local_path.joinpath("setup.py").touch()

    monkeypatch.setattr(
        sys, "argv", ["cibuildwheel", "--only", "cp313-manylinux_riscv64", str(local_path)]
    )

    main()

    _, err = capsys.readouterr()
    print(err)
    assert "'cpython-experimental-riscv64' enable is deprecated" not in err


@pytest.mark.usefixtures("platform", "intercepted_build_args")
def test_riscv64_no_warning2(monkeypatch, capsys, tmp_path):
    local_path = tmp_path / "tmp_project"
    os.mkdir(local_path)  # noqa:PTH102 Path.mkdir has been monkeypatched already
    local_path.joinpath("setup.py").touch()

    monkeypatch.setattr(
        sys, "argv", ["cibuildwheel", "--only", "cp313-manylinux_riscv64", str(local_path)]
    )
    monkeypatch.setenv("CIBW_ENABLE", "all")

    main()

    _, err = capsys.readouterr()
    print(err)
    assert "'cpython-experimental-riscv64' enable is deprecated" not in err


@pytest.mark.parametrize(
    ("architecture", "image", "full_image"),
    [
        ("x86_64", None, "quay.io/pypa/manylinux_2_28_x86_64:*"),
        ("x86_64", "manylinux2014", "quay.io/pypa/manylinux2014_x86_64:*"),
        ("x86_64", "manylinux_2_28", "quay.io/pypa/manylinux_2_28_x86_64:*"),
        ("x86_64", "manylinux_2_34", "quay.io/pypa/manylinux_2_34_x86_64:*"),
        ("x86_64", "custom_image", "custom_image"),
        ("i686", None, "quay.io/pypa/manylinux_2_28_i686:*"),
        ("i686", "manylinux2014", "quay.io/pypa/manylinux2014_i686:*"),
        ("i686", "manylinux_2_28", "quay.io/pypa/manylinux_2_28_i686:*"),
        ("i686", "custom_image", "custom_image"),
        ("pypy_x86_64", None, "quay.io/pypa/manylinux_2_28_x86_64:*"),
        ("pypy_x86_64", "manylinux2014", "quay.io/pypa/manylinux2014_x86_64:*"),
        ("pypy_x86_64", "manylinux_2_28", "quay.io/pypa/manylinux_2_28_x86_64:*"),
        ("pypy_x86_64", "manylinux_2_34", "quay.io/pypa/manylinux_2_34_x86_64:*"),
        ("pypy_x86_64", "custom_image", "custom_image"),
    ],
)
def test_manylinux_images(
    architecture, image, full_image, platform, intercepted_build_args, monkeypatch
):
    if image is not None:
        monkeypatch.setenv("CIBW_MANYLINUX_" + architecture.upper() + "_IMAGE", image)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    if platform == "linux":
        assert fnmatch(
            build_options.manylinux_images[architecture],
            full_image,
        )
    else:
        assert build_options.manylinux_images is None


def get_default_repair_command(platform: str) -> str:
    if platform == "linux":
        return "auditwheel repair -w {dest_dir} {wheel}"
    elif platform == "macos":
        return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
    elif platform == "windows" or platform == "pyodide":
        return ""
    else:
        msg = f"Unknown platform: {platform!r}"
        raise ValueError(msg)


@pytest.mark.parametrize("repair_command", [None, "repair", "repair -w {dest_dir} {wheel}"])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_repair_command(
    repair_command, platform_specific, platform, intercepted_build_args, monkeypatch
):
    if repair_command is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_REPAIR_WHEEL_COMMAND_" + platform.upper(), repair_command)
            monkeypatch.setenv("CIBW_REPAIR_WHEEL_COMMAND", "overwritten")
        else:
            monkeypatch.setenv("CIBW_REPAIR_WHEEL_COMMAND", repair_command)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    expected_repair = repair_command or get_default_repair_command(platform)
    assert build_options.repair_command == expected_repair


@pytest.mark.parametrize(
    "environment",
    [{}, {"something": "value"}, {"something": "value", "something_else": "other_value"}],
)
@pytest.mark.parametrize("platform_specific", [False, True])
def test_environment(environment, platform_specific, platform, intercepted_build_args, monkeypatch):
    env_string = " ".join(f"{k}={v}" for k, v in environment.items())
    if platform_specific:
        monkeypatch.setenv("CIBW_ENVIRONMENT_" + platform.upper(), env_string)
        monkeypatch.setenv("CIBW_ENVIRONMENT", "overwritten")
    else:
        monkeypatch.setenv("CIBW_ENVIRONMENT", env_string)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)
    intercepted_environment = build_options.environment

    assert isinstance(intercepted_environment, ParsedEnvironment)
    assert intercepted_environment.as_dictionary(prev_environment={}) == environment


@pytest.mark.parametrize("test_requires", [None, "requirement other_requirement"])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_test_requires(
    test_requires, platform_specific, platform, intercepted_build_args, monkeypatch
):
    if test_requires is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_TEST_REQUIRES_" + platform.upper(), test_requires)
            monkeypatch.setenv("CIBW_TEST_REQUIRES", "overwritten")
        else:
            monkeypatch.setenv("CIBW_TEST_REQUIRES", test_requires)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    assert build_options.test_requires == (test_requires or "").split()


@pytest.mark.parametrize("test_extras", [None, "extras"])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_test_extras(test_extras, platform_specific, platform, intercepted_build_args, monkeypatch):
    if test_extras is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_TEST_EXTRAS_" + platform.upper(), test_extras)
            monkeypatch.setenv("CIBW_TEST_EXTRAS", "overwritten")
        else:
            monkeypatch.setenv("CIBW_TEST_EXTRAS", test_extras)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    assert build_options.test_extras == ("[" + test_extras + "]" if test_extras else "")


@pytest.mark.parametrize("test_command", [None, "test --command"])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_test_command(
    test_command, platform_specific, platform, intercepted_build_args, monkeypatch
):
    if test_command is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_TEST_COMMAND_" + platform.upper(), test_command)
            monkeypatch.setenv("CIBW_TEST_COMMAND", "overwritten")
        else:
            monkeypatch.setenv("CIBW_TEST_COMMAND", test_command)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    assert build_options.test_command == (test_command or "")


@pytest.mark.parametrize("before_build", [None, "before --build"])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_before_build(
    before_build, platform_specific, platform, intercepted_build_args, monkeypatch
):
    if before_build is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_BEFORE_BUILD_" + platform.upper(), before_build)
            monkeypatch.setenv("CIBW_BEFORE_BUILD", "overwritten")
        else:
            monkeypatch.setenv("CIBW_BEFORE_BUILD", before_build)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)
    assert build_options.before_build == (before_build or "")


@pytest.mark.parametrize("build_verbosity", [None, 0, 2, -2, 4, -4])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_build_verbosity(
    build_verbosity, platform_specific, platform, intercepted_build_args, monkeypatch
):
    if build_verbosity is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_BUILD_VERBOSITY_" + platform.upper(), str(build_verbosity))
            monkeypatch.setenv("CIBW_BUILD_VERBOSITY", "overwritten")
        else:
            monkeypatch.setenv("CIBW_BUILD_VERBOSITY", str(build_verbosity))

    main()
    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    expected_verbosity = max(-3, min(3, int(build_verbosity or 0)))
    assert build_options.build_verbosity == expected_verbosity


@pytest.mark.parametrize("platform_specific", [False, True])
def test_config_settings(platform_specific, platform, intercepted_build_args, monkeypatch):
    config_settings = (
        'setting=value setting=value2 triplet=1 triplet=2 triplet=3 other="something else"'
    )
    if platform_specific:
        monkeypatch.setenv("CIBW_CONFIG_SETTINGS_" + platform.upper(), config_settings)
        monkeypatch.setenv("CIBW_CONFIG_SETTINGS", "a=b")
    else:
        monkeypatch.setenv("CIBW_CONFIG_SETTINGS", config_settings)

    main()
    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    assert build_options.config_settings == config_settings

    assert _split_config_settings(config_settings) == [
        "-Csetting=value",
        "-Csetting=value2",
        "-Ctriplet=1",
        "-Ctriplet=2",
        "-Ctriplet=3",
        "-Cother=something else",
    ]
    assert parse_config_settings(config_settings) == {
        "setting": ["value", "value2"],
        "triplet": ["1", "2", "3"],
        "other": "something else",
    }


@pytest.mark.parametrize(
    "selector",
    [
        "CIBW_BUILD",
        "CIBW_SKIP",
        "CIBW_TEST_SKIP",
    ],
)
@pytest.mark.parametrize(
    "pattern",
    [
        "cp27-*",
        "cp35-*",
        "?p36-*",
        "?p27*",
        "?p2*",
        "?p35*",
    ],
)
@pytest.mark.usefixtures("platform", "intercepted_build_args", "allow_empty")
def test_build_selector_deprecated_error(monkeypatch, selector, pattern, capsys):
    monkeypatch.setenv(selector, pattern)
    monkeypatch.delenv("CIBW_ENABLE", raising=False)

    if selector == "CIBW_BUILD":
        with pytest.raises(SystemExit) as ex:
            main()
        assert ex.value.code == 4

    else:
        main()

    stderr = capsys.readouterr().err
    series = "2" if "6" in pattern else "1"
    msg = f"cibuildwheel 3.x no longer supports Python < 3.8. Please use the {series}.x series or update"
    assert msg in stderr


@pytest.mark.parametrize("before_all", ["", None, "test text"])
@pytest.mark.parametrize("platform_specific", [False, True])
def test_before_all(before_all, platform_specific, platform, intercepted_build_args, monkeypatch):
    if before_all is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_BEFORE_ALL_" + platform.upper(), before_all)
            monkeypatch.setenv("CIBW_BEFORE_ALL", "overwritten")
        else:
            monkeypatch.setenv("CIBW_BEFORE_ALL", before_all)

    main()

    build_options = intercepted_build_args.args[0].build_options(identifier=None)

    assert build_options.before_all == (before_all or "")


@pytest.mark.parametrize(
    "dependency_versions",
    [None, "pinned", "latest", "FILE", "packages: pip==21.0.0"],
)
@pytest.mark.parametrize("platform_specific", [False, True])
def test_dependency_versions(
    dependency_versions, platform_specific, platform, intercepted_build_args, monkeypatch, tmp_path
):
    option_value = dependency_versions

    if dependency_versions == "FILE":
        constraints_file = tmp_path / "constraints.txt"
        constraints_file.write_text("foo==1.2.3\nbar==4.5.6")
        option_value = str(constraints_file)

    if option_value is not None:
        if platform_specific:
            monkeypatch.setenv("CIBW_DEPENDENCY_VERSIONS_" + platform.upper(), option_value)
            monkeypatch.setenv("CIBW_DEPENDENCY_VERSIONS", "overwritten")
        else:
            monkeypatch.setenv("CIBW_DEPENDENCY_VERSIONS", option_value)

    main()

    build_options: BuildOptions = intercepted_build_args.args[0].build_options(identifier=None)
    dependency_constraints = build_options.dependency_constraints
    if dependency_versions is None or dependency_versions == "pinned":
        assert dependency_constraints == DependencyConstraints.pinned()
    elif dependency_versions == "latest":
        assert dependency_constraints == DependencyConstraints.latest()
    elif dependency_versions == "FILE":
        assert dependency_constraints.base_file_path
        assert dependency_constraints.base_file_path.samefile(Path(option_value))
    elif dependency_versions.startswith("packages:"):
        assert dependency_constraints.packages == ["pip==21.0.0"]


@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"])
def test_debug_traceback(monkeypatch, method, capfd):
    if method == "command_line":
        monkeypatch.setattr(sys, "argv", [*sys.argv, "--debug-traceback"])
    elif method == "env_var":
        monkeypatch.setenv("CIBW_DEBUG_TRACEBACK", "TRUE")

    # set an option that produces a configuration error
    monkeypatch.setenv("CIBW_BUILD_FRONTEND", "invalid_value")

    with pytest.raises(SystemExit) as exit:
        main()
    assert exit.value.code == 2

    _, err = capfd.readouterr()

    if method == "unset":
        assert "Traceback (most recent call last)" not in err
    else:
        assert "Traceback (most recent call last)" in err


@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"])
def test_enable(method, intercepted_build_args, monkeypatch):
    monkeypatch.delenv("CIBW_ENABLE", raising=False)

    if method == "command_line":
        monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "pypy", "--enable", "graalpy"])
    elif method == "env_var":
        monkeypatch.setenv("CIBW_ENABLE", "pypy graalpy")

    main()

    enable_groups = intercepted_build_args.args[0].globals.build_selector.enable

    if method == "unset":
        assert enable_groups == frozenset()
    else:
        assert enable_groups == frozenset([EnableGroup.PyPy, EnableGroup.GraalPy])


def test_enable_all(intercepted_build_args, monkeypatch):
    monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "all"])

    main()

    enable_groups = intercepted_build_args.args[0].globals.build_selector.enable
    assert enable_groups == EnableGroup.all_groups()


def test_enable_arg_inherits(intercepted_build_args, monkeypatch):
    monkeypatch.setenv("CIBW_ENABLE", "pypy graalpy")
    monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "cpython-prerelease"])

    main()

    enable_groups = intercepted_build_args.args[0].globals.build_selector.enable

    assert enable_groups == frozenset(
        (EnableGroup.PyPy, EnableGroup.GraalPy, EnableGroup.CPythonPrerelease)
    )


def test_enable_arg_error_message(monkeypatch, capsys):
    monkeypatch.setattr(sys, "argv", [*sys.argv, "--enable", "invalid_group"])

    with pytest.raises(SystemExit) as ex:
        main()
    assert ex.value.code == 2

    _, err = capsys.readouterr()
    assert "Valid group names are:" in err


def test_defaults(platform, intercepted_build_args):
    main()

    build_options: BuildOptions = intercepted_build_args.args[0].build_options(identifier=None)
    with resources.DEFAULTS.open("rb") as f:
        defaults_toml = tomllib.load(f)

    root_defaults = defaults_toml["tool"]["cibuildwheel"]
    platform_defaults = defaults_toml["tool"]["cibuildwheel"][platform]

    defaults = {}
    defaults.update(root_defaults)
    defaults.update(platform_defaults)

    # test a few options
    assert build_options.before_all == defaults["before-all"]
    repair_wheel_default = defaults["repair-wheel-command"]
    if isinstance(repair_wheel_default, list):
        repair_wheel_default = " && ".join(repair_wheel_default)
    assert build_options.repair_command == repair_wheel_default
    assert build_options.build_frontend.name == "build"
    assert build_options.build_frontend.args == ()

    if platform == "linux":
        assert build_options.manylinux_images
        pinned_images = _get_pinned_container_images()
        default_x86_64_image = pinned_images["x86_64"][defaults["manylinux-x86_64-image"]]
        assert build_options.manylinux_images["x86_64"] == default_x86_64_image
